package network

import (
	"encoding/json"
	"github.com/Sirupsen/logrus"
	"net"
	"os"
	"path"
	"strings"
)

const ipamDefaultAllocatorPath = "/var/run/jodocker/network/ipam/subnet.json"

// IPAM 用于网络ip地址的分配和释放，包括容器的ip地址和网络网关的ip地址。
type IPAM struct {
	SubnetAllocatorPath string             // ip分配的数据存储文件路径
	Subnets             *map[string]string // 网段和位图算法的数组字符串, key:网段, value:分配的位图数组字符串
}

var ipAllocator = &IPAM{
	SubnetAllocatorPath: ipamDefaultAllocatorPath,
}

// 从subnet.json文件中加载网段ip分配信息
func (this *IPAM) load() error {
	// 判断subnet.json文件是否存在, 不存在则不需要加载
	if _, err := os.Stat(this.SubnetAllocatorPath); err != nil {
		if os.IsNotExist(err) {
			return nil
		} else {
			return err
		}
	}
	subnetConfigFile, err := os.Open(this.SubnetAllocatorPath)
	defer subnetConfigFile.Close()
	if err != nil {
		return err
	}

	// 读取subnet.json文件数据
	subnetJson := make([]byte, 2000)
	n, err := subnetConfigFile.Read(subnetJson)
	if err != nil {
		return err
	}

	// 反序列化为网段Map(网段ip分配信息)
	err = json.Unmarshal(subnetJson[:n], this.Subnets)
	if err != nil {
		logrus.Errorf("Error dump allocation info, %v", err)
		return err
	}
	return nil
}

// 网段ip分配信息存储到文件中
func (this *IPAM) dump() error {
	// 创建目录
	ipamConfigFileDir, _ := path.Split(this.SubnetAllocatorPath)
	if _, err := os.Stat(ipamConfigFileDir); err != nil {
		if os.IsNotExist(err) {
			os.MkdirAll(ipamConfigFileDir, 0644)
		} else {
			return err
		}
	}
	// 打开文件, 存在:清空, 不存在:创建
	subnetConfigFile, err := os.OpenFile(this.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
	defer subnetConfigFile.Close()
	if err != nil {
		return err
	}

	// 网段的ip分配map写入到subnet.json文件中
	ipamConfigJson, err := json.Marshal(this.Subnets)
	if err != nil {
		return err
	}
	_, err = subnetConfigFile.Write(ipamConfigJson)
	if err != nil {
		return err
	}
	return nil
}

// Allocate 从subnet网段中分配一个可用的IP
func (this *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) {
	// 存储网段中地址分配信息的数组
	this.Subnets = &map[string]string{}

	// 从文件中加载已经分配的网段信息
	err = this.load()
	if err != nil {
		logrus.Errorf("Error dump allocation info, %v", err)
	}

	// net.IPNet.Mask.Size()函数会返回网段的子网掩码的总长度和网段前面的固定位的长度
	// "127.0.0.0/8" 网段的子网掩码是 255.0.0.0
	// 那么subnet.Mask.Size()的返回值就是前面的255所对应的位数和总位数 即8和24
	_, subnet, _ = net.ParseCIDR(subnet.String())
	one, size := subnet.Mask.Size()

	// 如果之前没有分配过这个网段,则初始化网段的分配配置
	if _, exist := (*this.Subnets)[subnet.String()]; !exist {
		// 用"0"填满这个网段的配置, 1 << uint8(size-one) 表示这个网段中有多少个可用地址.
		// size-one是子网掩码后面的网络位数, 2^(size-one)表示这个网段中可用ip数量
		// 而2^(size-one)等价于 1 << uint(size-on)
		(*this.Subnets)[subnet.String()] = strings.Repeat("0", 1<<uint8(size-one))
	}

	// 遍历网段的位图数组
	for c := range (*this.Subnets)[subnet.String()] {
		// 找到数组中未"0"的项和数组序号, 即可以分配的ip
		if (*this.Subnets)[subnet.String()][c] == '0' {
			// 设置这个为0的序号值为1, 即分配这个ip
			ipAlloc := []byte((*this.Subnets)[subnet.String()])
			ipAlloc[c] = '1'
			(*this.Subnets)[subnet.String()] = string(ipAlloc)
			// 这里的ip为初始ip, 比如对于网段192.168.0.0/16, 192.168.0.0就是初始ip
			ip = subnet.IP

			// 通过网段的ip与上面的偏移相加计算出分配的ip地址, 由于ip地址是uint的数组,需要通过数组中的每一项加所需要的值
			// 比如网段是127.16.0.0/12, 数组序号是65555,那么在[172,16,0,0]上依次加[uint(65555>>24),uint(65555>>16),uint(65555>>8),uint(65555>>0)],  uint(65555>>0)为最后的8位字节数算的是19
			// 即[0,1,0,19]那么获得的ip就是172.17.0.19
			// 根据数组序号来计算ip地址
			for t := uint(4); t > 0; t -= 1 {
				[]byte(ip)[4-t] += uint8(c >> ((t - 1) * 8))
			}
			// 由于此处ip是从1开始分配的, 所以最后再加1, 最终得到分配的ip是172.17.0.20
			ip[3] += 1
			break
		}
	}
	// ip分配写入文件中
	this.dump()
	return
}

// Release 从指定的subnet网段中释放指定的ip地址
func (this *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error {
	this.Subnets = &map[string]string{}
	_, subnet, _ = net.ParseCIDR(subnet.String())
	err := this.load()
	if err != nil {
		logrus.Errorf("Error dump allocation info, %v", err)
	}

	// 计算ip地址在网段位图数组中的索引位置
	c := 0
	// 将ip地址转换成4个字节的表示方式
	releaseIP := ipaddr.To4()
	// 由于ip是从1开始分配的, 所以转换成所以应减1
	releaseIP[3] -= 1
	for t := uint(4); t > 0; t -= 1 {
		// 与分配ip相反, 释放ip获得索引的方式是ip地址的每一位相减之后分别左移将对应的数值加到索引上
		c += int(releaseIP[t-1]-subnet.IP[t-1]) << ((4 - t) * 8)
	}
	// 将分配的位图数组中的索引位置的值置为0
	ipAlloc := []byte((*this.Subnets)[subnet.String()])
	ipAlloc[c] = '0'
	(*this.Subnets)[subnet.String()] = string(ipAlloc)

	// 保存释放掉ip之后的网段ip分配信息
	this.dump()
	return nil
}
